using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
using WolvenKit.Common.Model;
using WolvenKit.Core.Compression;
using WolvenKit.Core.Exceptions;
using WolvenKit.Core.Extensions;
using WolvenKit.Core.Helpers;
using WolvenKit.Core.Unmanaged;
using WolvenKit.RED4.Types;
using WolvenKit.RED4.Types.Pools;

namespace WolvenKit.Common.Services
{
    public class HashService : IHashService
    {
        #region Fields

        private const string s_used = "WolvenKit.Common.Resources.usedhashes.kark";
        private const string s_nodeRefs = "WolvenKit.Common.Resources.noderefs.kark";
        private const string s_tweakDbStr = "WolvenKit.Common.Resources.tweakdbstr.kark";
        private const string s_missing = "WolvenKit.Common.Resources.missinghashes.json";

        private static readonly int _maxDoP = Environment.ProcessorCount > 2 ? (Environment.ProcessorCount - 2) : 1;

        private readonly Dictionary<ulong, SAsciiString> _hashes = new();

        private Dictionary<ulong, string> _missing = new();

        private volatile bool _isLoaded;
        private readonly TaskCompletionSource _loader = new();

        public Task Loaded => _loader.Task;

        #endregion Fields

        #region Constructors

        public HashService() : this(true)
        {
        }

        public HashService(bool autoLoad)
        {
            if (autoLoad)
            {
                Load();
            }
        }

        #endregion Constructors

        #region Methods

        public void Load()
        {
            if (_isLoaded)
            {
                return;
            }

            try
            {
                var hashesMemory = DecompressEmbeddedFile(s_used);
                ReadHashes(hashesMemory.GetStream());

                hashesMemory.Dispose();

                var nodeRefsMemory = DecompressEmbeddedFile(s_nodeRefs);
                ReadNodeRefs(nodeRefsMemory.GetStream());

                nodeRefsMemory.Dispose();

                var tweakNamesMemory = DecompressEmbeddedFile(s_tweakDbStr);
                ReadTweakNames(tweakNamesMemory.GetStream());

                tweakNamesMemory.Dispose();

                LoadMissingHashes();

                _isLoaded = true;
                _loader.SetResult();
            }
            catch (Exception e)
            {
                _loader.SetException(e);
                throw;
            }
        }

        public IEnumerable<ulong> GetAllHashes() => _hashes.Keys;

        public IEnumerable<ulong> GetMissingHashes() => _missing.Keys;

        public bool Contains(ulong key, bool checkUserHashes = true)
        {
            if (ResourcePathPool.IsNative(key))
            {
                return true;
            }

            if (checkUserHashes && ResourcePathPool.IsRuntime(key))
            {
                return true;
            }

            if (_missing.ContainsKey(key))
            {
                return false;
            }

            return false;
        }

        public string? GetGuessedExtension(ulong key)
        {
            if (_missing.TryGetValue(key, out var ext))
            {
                return ext;
            }
            return null;
        }

        public string? Get(ulong key)
        {
            if (ResourcePathPool.ResolveHash(key) is { } value)
            {
                return value;
            }

            return null;
        }

        private static unsafe UnmanagedMemory DecompressEmbeddedFile(string resourceName)
        {
            using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName).NotNull();

            // read KARK header
            var oodleCompression = stream.ReadStruct<uint>();

            if (oodleCompression != Oodle.KARK)
            {
                throw new DecompressionException("Incorrect hash file.");
            }

            var outputSize = stream.ReadStruct<uint>();

            var compressedBufferLength = (int)(stream.Length - (sizeof(uint) * 2));
            using var compressedBuffer = UnmanagedMemory.Allocate(compressedBufferLength);
            var decompressedBuffer = UnmanagedMemory.Allocate((int) outputSize);

            // read the rest of the stream
            var read = stream.Read(compressedBuffer.GetSpan());

            if (read != compressedBufferLength)
            {
                throw new InvalidOperationException("Read less bytes than expected!");
            }

            Oodle.Decompress(
                compressedBuffer.Pointer, compressedBuffer.Size,
                decompressedBuffer.Pointer, decompressedBuffer.Size);

            return decompressedBuffer;
        }

        private void ProcessLinesConcurrently(Stream memoryStream, Action<string> lineAction)
        {
            var collection = new BlockingCollection<string>();

            var readerTask = Task.Run(() =>
            {
                using var sr = new StreamReader(memoryStream);

                while (true)
                {
                    var nextLine = sr.ReadLine();

                    if (nextLine == null)
                    {
                        break;
                    }

                    collection.Add(nextLine);
                }

                collection.CompleteAdding();
            });

            Parallel.ForEach(collection.GetConsumingEnumerable(), new ParallelOptions
            {
                MaxDegreeOfParallelism = _maxDoP,
            }, lineAction);

            readerTask.Wait();
        }

        private void LoadMissingHashes()
        {
            using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(s_missing).NotNull();
            _missing = JsonSerializer.Deserialize<Dictionary<ulong, string>>(stream).NotNull();
        }

        private void ReadHashes(Stream memoryStream)
        {
            var collection = new List<string>();

            using var sr = new StreamReader(memoryStream);
            while (true)
            {
                var nextLine = sr.ReadLine();
                if (nextLine == null)
                {
                    break;
                }
                collection.Add(nextLine);
            }

            var lookupTable = new LookupTable(collection, _maxDoP, ResourcePath.CalculateHash);

            ResourcePathPool.SetNative(lookupTable);
        }

        private void ReadNodeRefs(Stream memoryStream)
        {
            var collection = new List<string>();

            using var sr = new StreamReader(memoryStream);
            while (true)
            {
                var nextLine = sr.ReadLine();
                if (nextLine == null)
                {
                    break;
                }
                collection.Add(nextLine);
            }

            var lookupTable = new LookupTable(collection, _maxDoP, ResourcePath.CalculateHash);

            NodeRefPool.SetNative(lookupTable);
        }

        private void ReadTweakNames(Stream memoryStream)
        {
            var collection = new List<string>();

            using var br = new BinaryReader(memoryStream);

            // skip header
            br.BaseStream.Position = 20;

            while (br.BaseStream.Position < br.BaseStream.Length)
            {
                collection.Add(br.ReadLengthPrefixedString());
            }

            var lookupTable = new LookupTable(collection, _maxDoP, TweakDBID.CalculateHash);

            TweakDBIDPool.SetNative(lookupTable);
        }

        #endregion Methods
    }
}
